-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local def = require 'biz_topo.def'
local mdb = require 'mc.mdb' 
local client = require 'pcie_device.client' 
local mc_signal = require 'mc.signal' 
local imu_cmd = require 'imu' 
local class = require 'mc.class' 
local log = require 'mc.logging' 
local cmn = require 'common' 
local common_interface = require 'common_interface'

local DEFAULT_INTERFACE<const> = 'bmc.kepler.Systems.PCIeDevices.PCIeDevice'
local UINT8_MAX<const> = 0xff
---@class PCIeDevice @PCIe设备
---@field public mds table @MDS对象
local c_pcie_device = class(nil, nil, true)

function c_pcie_device:get_prop(name, interface)
    if not self.mds or not name then
        return nil
    end

    if interface and self.mds[interface] then
        return self.mds[interface][name]
    elseif self.mds[name] then
        return self.mds[name]
    elseif self.mds[DEFAULT_INTERFACE] then
        return self.mds[DEFAULT_INTERFACE][name]
    end

    return nil
end

function c_pcie_device:set_prop(name, val, interface)
    if not self.mds or not name then
        return nil
    end

    if interface and self.mds[interface] and self.mds[interface][name] ~= nil then
        self.mds[interface][name] = val
    elseif self.mds[name] then
        self.mds[name] = val
    elseif self.mds[DEFAULT_INTERFACE] and self.mds[DEFAULT_INTERFACE][name] ~= nil then
        self.mds[DEFAULT_INTERFACE][name] = val
    end

    return nil
end

function c_pcie_device:update_bus_info(segment, socket_id, bus, device, func)
    self.mds.Segment = segment
    self.mds.SocketID = socket_id
    self.mds.Bus = bus
    self.mds.Device = device
    self.mds.Function = func
end

-- 更新设备BDF的时候，查询并更新ClassCode
local function get_pcie_class_code_info(sys_id, bus, socket_id, dev_bus, dev_device, dev_function)
    local payload = {
        is_local = false,
        cpu_id = socket_id,
        bus_num = dev_bus,
        device_num = dev_device,
        function_num = dev_function,
        address = imu_cmd.pci_info_address_list.CLASS_CODE_INFO_ADDR,
        read_length = 4
    }
    -- 若失败，可尝试20次获取。尝试20次都失败，将所有字段都置为0xFE，表明获取class code失败，class信息未知
    local info
    for _ = 1, 20 do
        info = imu_cmd.get_info_from_pmu(sys_id, bus, payload)
        if info == nil or #info < 4 then
            log:debug('[pcie_device] get_info_from_pmu failed')
        else
            break
        end
        cmn.skynet.sleep(10) -- 100ms
    end
    if info == nil or #info < 4 then
        return 0xFE, 0xFE, 0xFE
    end
    log:debug('[pcie_device] Get class code from PMU, base_class:%s, sub_class:%s, program_interface:%s',
        info[4], info[3], info[2])
    return info[4], info[3], info[2]
end

-- 通过BDF查询PCI配置空间里的ClassCode
function c_pcie_device:query_class_code_from_pmu()
    if self:get_prop('DevBus') == UINT8_MAX or self:get_prop('DeviceType') ~= def.com_type.PCIE_CARD then
        log:debug('[pcie_device] query_class_code_from_pmu DeviceName=%s DevBus=%s DeviceType=%s.', 
            self:get_prop('DeviceName'), self:get_prop('DevBus'), self:get_prop('DeviceType'))
        error("DevBus is 0xff or DeviceType error.")
    end
    local sys_id = self:get_host_id_by_presence()
    -- 如果OS下电，就不需要查询
    local power_state = common_interface.get_instance().power_status[sys_id]
    if power_state == nil or power_state == 'OFF' then
        error("[pcie_device] power off")
    end
    local err, base_class_code, sub_class_code, program_interface
    err, base_class_code, sub_class_code, program_interface = pcall(get_pcie_class_code_info, sys_id, self.bus,
        self:get_prop('SocketID'), self:get_prop('DevBus'),
        self:get_prop('DevDevice'), self:get_prop('DevFunction'))
    if not err then
        error('[pcie_device] Get class_code from pmu failed, %s', err)
    end
    local str_class_code = string.format('%06x', ((base_class_code & 0xff) << 16) + ((sub_class_code & 0xff) << 8) +
        (program_interface & 0xff))
    log:debug('[pcie_device] Get class_code from PMU, class_code:%s bdf=%s-%s-%s', str_class_code,
        self:get_prop('DevBus'), self:get_prop('DevDevice'), self:get_prop('DevFunction'))
    self:set_prop('BaseClassCode', base_class_code)
    self:set_prop('SubClassCode', sub_class_code)
    self:set_prop('ProgrammingInterface', program_interface)
end

function c_pcie_device:query_from_pmu_task()
    cmn.skynet.fork_loop({count = 0}, function ()
        local ok, err
        while true do
            if not self.class_code_need_update then
                goto next
            end
            ok, err = pcall(function()
                self:query_class_code_from_pmu()
            end)
            if not ok then
                log:debug('[pcie_device] Error=%s', err)
            end
            ::next::
            cmn.skynet.sleep(60 * 100)
        end
    end)
end

-- 根据MultihostPresence获取对应的sys_id
function c_pcie_device:get_host_id_by_presence()
    local multi_host_presence = self:get_prop('MultihostPresence')
    local cur_sys_id = 1
    while multi_host_presence > 0 do
        if multi_host_presence & 1 > 0 then
            return cur_sys_id
        end
        multi_host_presence = multi_host_presence >> 1
        cur_sys_id = cur_sys_id + 1
    end
    log:debug('[pcie_device] get_host_id_by_presence fail. DeviceName=%s MultihostPresence=%s.', 
        self:get_prop('DeviceName'), self:get_prop('MultihostPresence'))
end

-- PcieAddrInfo Property : PCIeDevice Property
local prop_map = {
    ['SlotID'] = function (obj, value)
        obj:set_prop('SlotID', value)
        obj:update_pciedevice_devicename()
    end,
    ['Segment'] = 'Segment',
    ['SocketID'] = 'SocketID',
    ['Bus'] = 'Bus',
    ['Device'] = 'Device',
    ['Function'] = 'Function',
    ['DevBus'] = 'DevBus',
    ['DevDevice'] = 'DevDevice',
    ['DevFunction'] = 'DevFunction',
    ['MultihostPresence'] = 'MultihostPresence'
}

-- 从PcieAddrInfo对象同步属性值到当前PCIeDevice对象
-- 1. 关联PcieAddrInfo对象，对其属性变化建立监听
-- 2. 立即同步一次属性值
-- 3. 释放同步完成信号：sync_pai_to_device_complete
function c_pcie_device:sync_pcie_addr_info(pcie_addr_info_obj)
    -- 注册属性变化的监听
    pcie_addr_info_obj.mds_obj.property_changed:on(
        function(name, value, sender)
            if name == 'SlotID' then
                prop_map[name](self, value)
                return
            end
            if prop_map[name] then
                self:set_prop(prop_map[name], value)
            end
            self.class_code_need_update = true
        end)
    -- 同步一次属性值
    local value
    local ok, err
    for addr_prop_name, device_prop_name in pairs(prop_map) do
        if type(device_prop_name) == 'string' then
            log:notice('sync_pcie_addr_info, name=%s, value=%s, position=%s, src_name=%s', device_prop_name,
                pcie_addr_info_obj:get_prop(addr_prop_name), self.position, pcie_addr_info_obj.mds_obj.name)
            self:set_prop(device_prop_name, pcie_addr_info_obj:get_prop(addr_prop_name))
        else
            value = pcie_addr_info_obj:get_prop(addr_prop_name)
            log:notice('sync_pcie_addr_info, name=%s, value=%s, position=%s, src_name=%s', addr_prop_name, value,
                self.position, pcie_addr_info_obj.mds_obj.name)
            ok, err = pcall(function()
                prop_map[addr_prop_name](self, value)
            end)
            if not ok then
                log:warn('sync_pcie_addr_info, not ok, err=%s', err)
            end
        end
    end
    self.ref_pcie_addr_info = pcie_addr_info_obj

    -- 释放完成信号
    self.sync_pai_to_device_complete:emit(self, pcie_addr_info_obj)
    self.class_code_need_update = true

    log:notice('Sync PCIeAddrinfo completed, PCIeDevice position=%s', self.position)
end

function c_pcie_device:check_dev_prop_emit(prop)
    self.dev_prop_changed:emit(self:get_prop('ObjectName'), prop, self:get_prop(prop))
end

-- 获取Container对象，默认为上级器件的Component对象
function c_pcie_device:get_container_obj()
    local COMPONENT_PRE_PATH = '/bmc/kepler/Systems/' .. self.system_id .. '/Components/'
    local COMPONENT_INTERFACE = 'bmc.kepler.Systems.Component'

    local container_obj_name = self:get_prop('Container')
    if not container_obj_name or container_obj_name == '' then
        return false
    end
    -- 获取对应riser卡的Component 间隔5秒重试一次 最多100次
    local ok, obj = cmn.retry_func(500, 100, function()
        return pcall(mdb.get_object, self.bus, COMPONENT_PRE_PATH .. container_obj_name,
            COMPONENT_INTERFACE)
    end)
    if not ok then
        log:error('Get container obj failed, err=%s', obj)
        return false
    end
    return obj
end

-- 通过Container属性更新Position
function c_pcie_device:update_pciedevice_position()
    local container = self:get_container_obj()
    if not container then
        return
    else
        local position = container.Name
        self:set_prop('Position', position)
        -- 监听Name的变化
        client:OnComponentPropertiesChanged(function(values, path)
            if path ~= container.path or not values['Name'] then
                return
            end
            self:set_prop('Position', values['Name']:value())
        end)
    end
end

-- 更新DeviceName
function c_pcie_device:update_pciedevice_devicename()
    local device_name = self.origin_name
    local slot_id = self:get_prop('SlotID')
    if device_name == nil or device_name == '' then
        device_name = 'PCIe Card ' .. tostring(slot_id)
        self:set_prop('DeviceName', device_name)
    else
        device_name = string.gsub(device_name, '%$', tostring(slot_id), 1) -- 1表示替换1次
        self:set_prop('DeviceName', device_name)
    end
    self.device_name_ready = slot_id ~= 0
end

local function get_system_id(path)
    return tonumber(string.match(path, '/bmc/kepler/Systems/(%d+)'))
end 

function c_pcie_device:init()
    self.system_id = get_system_id(self.mds.path)
    cmn.skynet.fork(function ()
        self:update_pciedevice_position()
    end)

    -- 属性发生变化通知设备树对象管理
    self.mds.property_changed:on(function(name, value, sender)
        self:check_dev_prop_emit(name)
    end)
end

function c_pcie_device:ctor(mds_obj, position, bus)
    self.mds = mds_obj
    self.position = position
    self.bus = bus
    self.listening = {}
    self.device_name_ready = false
    self.origin_name = self.mds['DeviceName']
    -- 信号：从PcieAddrInfo对象同步属性值完成
    self.sync_pai_to_device_complete = mc_signal.new()
    -- 信号：设备树相关属性变化
    self.dev_prop_changed = mc_signal.new()
    self.class_code_need_update = false
end

return c_pcie_device
